<?php

namespace App\Services;

use App\Exceptions\InternalException;
use App\Exceptions\TokenInvalidException;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;

/**
 * @Author: dxf
 * @Date: 2021-07-19
 * @Time: 13:19
 */

/**
 * token服务类
 *
 */
class TokenService
{
    /**
     * hash_hmac默认秘钥
     * @var string
     */
    private $my_key = 'a21e2237ed2e0';

    /**
     * @var 配置
     */
    private $config;

    public function __construct()
    {
        $this->config = config('token');
    }

    /**
     * 生成用户数据对应的随机字符
     * @param $length
     * @param bool $status
     * @return bool|mixed|string
     */
    private function userToken($length, $status = false)
    {
        $randomChars = Str::random(32);
        //bcrypt是一种哈希算法
        $token = substr(bcrypt($randomChars . helper()->getMsectime() . $this->config['token_prefix']), $length);

        //替换‘/’
        if ($status) {
            $token = str_replace(['.', '/', '+'], ['a', '_', '-'], $token);
        }
        if (Redis::exists($token)) {
            $token = $this->userToken($length);
        }
        return $token;

    }

    /**
     * 校验算法
     * HMACSHA256加密
     * @param $payload
     * @return false|string
     * @author  dxf
     * @date    2022/10/25 10:30
     */
    public function hmacSha256($payload)
    {
        //把秘钥做成配置项
        $my_key = config('custom.salt', $this->my_key);
        return hash_hmac('sha256', $this->base64UrlEncode($payload), $my_key);
    }

    /**
     * url_encode(base64编码)
     * @param $data
     * @return string
     */
    private function base64UrlEncode($data)
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    /**
     * url_decode(base64编码)
     * @param $data
     * @return false|string
     */
    private function base64urlDecode($data)
    {
        return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
    }

    /**
     * 生成返回给用户的token
     * @param $payload
     * @param $signature
     * @return string
     * @author  dxf
     * @date    2022/10/25 10:52
     */
    public function generateToken($payload, $signature): string
    {
        //token组成 uid.timestamp.signature
        return base64_encode($payload . "." . helper()->getMsectime() . "." . $signature);
    }


    /**
     * 验证token 不包含用户对应随机字符验证
     * @param string $token 前端传过来的base64加密的token
     * @return bool
     * @throws TokenInvalidException
     * @author  dxf
     * @date    2022/10/25 11:11
     */
    public function checkToken($token): bool
    {
        //拆分成3段
        $check_arr = explode('.', base64_decode($token, true));
        return $this->checkTokenArr($check_arr);
    }

    /**
     * 验证token数组
     * @param $check_arr
     * @return bool
     * @throws TokenInvalidException
     * Date:    2024/8/15 17:39
     */
    public function checkTokenArr($check_arr): bool
    {
        if (count($check_arr) != 3) {
            throw new TokenInvalidException('无效Token');
        }

        // 重放攻击时间戳检测
//        if ((helper()->getMsectime() - $check_arr[1]) > $this->config['time_diff']) {
//            throw new TokenInvalidException('token过期');
//        }
        $check_hs256 = $this->hmacSha256($check_arr[2]);
        return $this->checkSignature($check_hs256);
    }


    /**
     * 验签
     * @return bool
     * @throws TokenInvalidException
     * @author  dxf
     * @date    2022/10/25 11:03
     */
    private function checkSignature(string $hs256_string): bool
    {
//        $check_hs256 = $this->hmacSha256($check_arr[2]);
//        $token_key = Redis::get('user:login:pool:'.$check_arr[0]);
//        if (!$token_key) {
//            throw new TokenInvalidException('token无效，用户不存在');
//        }
        $signature = Redis::exists($hs256_string);
        if (!$signature) {
            throw new TokenInvalidException('token无效，用户不存在');
        }

        return true;
    }


    /**
     * 组装token
     * @return array
     * @author  dxf
     * @date    2022/10/25 13:52
     */
    private function createToken()
    {
        $user_token = $this->userToken(7, true);
        $signature = $this->hmacSha256($user_token);
        return [$user_token, $signature];
    }

    /**
     * 生成token并把用户唯一标识保存到redis中
     * @param array $user_data 用户数据 拓展设备登录数控制功能可以在此json元素中插入
     * @param int $expire 有效期（s）
     * @param string $old_token 旧的token  如果要刷新token则传值
     * @return bool|mixed|string
     * @throws InternalException|TokenInvalidException
     * @author  dxf
     * @date    2022/10/19 15:41
     */
    public function saveTokenToLoginPool($user_data, $expire = null, $old_token = null)
    {
        //user_flag规则 user company school
        if (!isset($user_data['id'])) {
            throw new TokenInvalidException('用户标识数据错误');
        }
        //验签并解析用户数据

        //如果有值  则删除旧的token
        if ($old_token) {
            $check_arr = explode('.', base64_decode($old_token, true));
            $this->checkTokenArr($check_arr);
            //删除旧token login pool目的是为了后台管理员可以管理 各个端的用户
            Redis::del('user:login:pool:' . $user_data['id']);
            $check_hs256 = $this->hmacSha256($check_arr[2]);
            Redis::del($check_hs256);
            //更换新token
        }

        //尽量不选hash类型，hash类型  site用户的唯一标识  value token:timestamp
        //hash类型不能精确的给每个元素设置过期时间 如果有用户只操作了一次之后就不操作了
        //就会成为死数据，没有触发删除的操作，还得写定时任务清理
        //保存token为string类型，用户数据为value的k/v到redis

        list($user_token, $signature) = $this->createToken();

        //设置登录池  可以设置登录设备 登录次数等 login pool目的是为了后台管理员可以管理 各个端的用户
        Redis::setex('user:login:pool:' . $user_data['id'], $expire, $signature);
        //关联token和数据
        $result = Redis::setex($signature, $expire, serialize($user_data));

        if (!$result) {
            throw new InternalException('网络异常');
        }

        return $user_token;
    }


    /**
     * 更新用户数据
     * @param $user_data
     * @param $token
     * @param $expire
     * @return bool
     * @throws InternalException
     * @throws TokenInvalidException
     * Date:    2024/5/3 13:11
     */
    public function updateUserData($user_data, $token, $expire = null)
    {
        if (!isset($user_data['id'])) {
            throw new TokenInvalidException('用户标识数据错误');
        }

        $check_arr = explode('.', base64_decode($token, true));
        $this->checkTokenArr($check_arr);
        //关联用户唯一标识和token
        $result = Redis::setex($check_arr[2], $expire, serialize($user_data));

        if (!$result) {
            throw new InternalException('网络异常');
        }
        return true;
    }


    /**
     * 更新token有效期
     * @param int $uid
     * @param string $signature
     * @param int $time
     * @return true
     * @throws InternalException Date:    2024/8/23 10:26
     */
    public function refreshToken(int $uid, string $signature, int $time): bool
    {
        $status1 = Redis::expire('user:login:pool:' . $uid, $time);
        $status2 = Redis::expire($signature, $time);
        if (!($status1 && $status2)) {
            throw new InternalException('网络异常');
        }
        return true;
    }

}
